/*
 * sculluid.c  create one device only permit one user access it 
 */

#include <linux/module.h>
#include <linux/init.h>

#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/cdev.h>

#include <asm/uaccess.h>
#include <asm/atomic.h>

#include "sculluid.h"

static int sculluid_major = 0;
static dev_t devno;
static struct sculluid_dev *sculluid_device;
static int sculluid_count;
static uid_t sculluid_owner;
static spinlock_t sculluid_lock = SPIN_LOCK_UNLOCKED; /* static */

/* spin_lock_init(&scullid_lock)  dynamic */

int scull_trim(struct sculluid_dev *dev)
{
	struct scull_qset *next, *dptr;
	int qset = dev->qset;   /* "dev" is not-null */
	int i;

	for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
		if (dptr->data) {
			for (i = 0; i < qset; i++)
				kfree(dptr->data[i]);
			kfree(dptr->data);
			dptr->data = NULL;
		}
		next = dptr->next;
		kfree(dptr);
	}
	dev->size = 0;
	dev->quantum = 4000;
	dev->qset = 1000;
	dev->data = NULL;
	return 0;
}

struct scull_qset *scull_follow(struct sculluid_dev *dev, int n)
{
	struct scull_qset *qs = dev->data;

        /* Allocate first qset explicitly if need be */
	if (! qs) {
		qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
		if (qs == NULL)
			return NULL;  /* Never mind */
		memset(qs, 0, sizeof(struct scull_qset));
	}

	/* Then follow the list */
	while (n--) {
		if (!qs->next) {
			qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
			if (qs->next == NULL)
				return NULL;  /* Never mind */
			memset(qs->next, 0, sizeof(struct scull_qset));
		}
		qs = qs->next;
		continue;
	}
	return qs;
}

static int sculluid_open(struct inode *inode, struct file *filp)
{
	struct sculluid_dev *dev = sculluid_device;
	
	spin_lock(&sculluid_lock);

	if (sculluid_count && (sculluid_owner != current->cred->uid) && /* allow user */
		(sculluid_owner != current->cred->euid) && /* allow whoever did su */
		!capable(CAP_DAC_OVERRIDE) ) { /* still allow root */
		spin_unlock(&sculluid_lock);
		return -EBUSY;
	}

	if (sculluid_count == 0)
		sculluid_owner = current->cred->uid;
	
	sculluid_count++;
	spin_unlock(&sculluid_lock);

	if ((filp->f_flags & O_ACCMODE) == O_WRONLY)
		scull_trim(dev);
	filp->private_data = dev;

	return 0;
}

ssize_t sculluid_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	struct sculluid_dev *dev = filp->private_data; 
	struct scull_qset *dptr;	/* the first listitem */
	int quantum = dev->quantum, qset = dev->qset;
	int itemsize = quantum * qset; /* how many bytes in the listitem */
	int item, s_pos, q_pos, rest;
	ssize_t retval = 0;

	if (down_interruptible(&dev->sem))
		return -ERESTARTSYS;
	if (*f_pos >= dev->size)
		goto out;
	if (*f_pos + count > dev->size)
		count = dev->size - *f_pos;

	/* find listitem, qset index, and offset in the quantum */
	item = (long)*f_pos / itemsize;
	rest = (long)*f_pos % itemsize;
	s_pos = rest / quantum; q_pos = rest % quantum;

	/* follow the list up to the right position (defined elsewhere) */
	dptr = scull_follow(dev, item);

	if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
		goto out; /* don't fill holes */

	/* read only up to the end of this quantum */
	if (count > quantum - q_pos)
		count = quantum - q_pos;

	if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
		retval = -EFAULT;
		goto out;
	}
	*f_pos += count;
	retval = count;

  out:
	up(&dev->sem);
	return retval;
}

ssize_t sculluid_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	struct sculluid_dev *dev = filp->private_data;
	struct scull_qset *dptr;
	int quantum = dev->quantum, qset = dev->qset;
	int itemsize = quantum * qset;
	int item, s_pos, q_pos, rest;
	ssize_t retval = -ENOMEM; /* value used in "goto out" statements */

	if (down_interruptible(&dev->sem))
		return -ERESTARTSYS;

	/* find listitem, qset index and offset in the quantum */
	item = (long)*f_pos / itemsize;
	rest = (long)*f_pos % itemsize;
	s_pos = rest / quantum; q_pos = rest % quantum;

	/* follow the list up to the right position */
	dptr = scull_follow(dev, item);
	if (dptr == NULL)
		goto out;
	if (!dptr->data) {
		dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
		if (!dptr->data)
			goto out;
		memset(dptr->data, 0, qset * sizeof(char *));
	}
	if (!dptr->data[s_pos]) {
		dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
		if (!dptr->data[s_pos])
			goto out;
	}
	/* write only up to the end of this quantum */
	if (count > quantum - q_pos)
		count = quantum - q_pos;

	if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
		retval = -EFAULT;
		goto out;
	}
	*f_pos += count;
	retval = count;

        /* update the size */
	if (dev->size < *f_pos)
		dev->size = *f_pos;

  out:
	up(&dev->sem);
	return retval;
}

static int sculluid_release(struct inode *inode, struct file *filp)
{
	spin_lock(&sculluid_lock);
	sculluid_count--; /* nothing else */
	spin_unlock(&sculluid_lock);
	return 0;
}


struct file_operations sculluid_fops = {
	.owner = THIS_MODULE,
	.open = sculluid_open,
	.read = sculluid_read,
	.write = sculluid_write,
	.release = sculluid_release
}; 

static int sculluid_init(void)
{
	int result, err;

	if (sculluid_major) {
                devno = MKDEV(sculluid_major, 0);
                result = register_chrdev_region(devno, 1, "sculluid");
        } else {
                result = alloc_chrdev_region(&devno, 0, 1, "sculluid");
                sculluid_major = MAJOR(devno);
        }
        if (result < 0) {
                printk(KERN_WARNING "sculluid: can't get major %d\n", sculluid_major);
                return result;
        }

	sculluid_device = kmalloc(sizeof(struct sculluid_dev), GFP_KERNEL);
	if (!sculluid_device) /* if (sculluid_device == NULL) */ {
                unregister_chrdev_region(devno, 1);
                return -ENOMEM;
        }

        memset(sculluid_device, 0, sizeof(struct sculluid_dev));

	sculluid_device->quantum = 4000;
	sculluid_device->qset = 1000;
	init_MUTEX(&sculluid_device->sem);

	cdev_init(&sculluid_device->cdev, &sculluid_fops);
	sculluid_device->cdev.owner = THIS_MODULE;
	err = cdev_add(&sculluid_device->cdev, devno, 1);
	if (err) {
		printk(KERN_NOTICE "Error %d adding sculluid_device.\n", err);
		kobject_put(&sculluid_device->cdev.kobj);
		goto fail;
	} else
		printk(KERN_NOTICE "sculluid_device registered at %x\n", devno);
	
	return 0;
fail:
	kfree(sculluid_device);
	unregister_chrdev_region(devno, 1);
	return err;
}

static void sculluid_cleanup(void)
{
	cdev_del(&sculluid_device->cdev);
	scull_trim(sculluid_device);
	kfree(sculluid_device);
	unregister_chrdev_region(devno, 1);
}

module_init(sculluid_init);
module_exit(sculluid_cleanup);

MODULE_AUTHOR("WXY");
MODULE_LICENSE("GPL");
